5.6 公开或未公开的标识符
要想设计出好的 API,需要使用某种规则来控制声明后的标识符的可见性。Go 语言支持从包里公开或者隐藏标识符。通过这个功能,让用户能按照自己的规则控制标识符的可见性。在第3章讨论包的时候,谈到了如何从一个包引入标识符到另一个包。有时候,你可能不希望公开包里的某个类型、函数或者方法这样的标识符。在这种情况,需要一种方法,将这些标识符声明为包外不可见,这时需要将这些标识符声明为未公开的。
让我们用一个示例程序来演示如何隐藏包里未公开的标识符,如代码清单5-64所示。
代码清单5-64 listing64/
counters/counters.go
-----------------------------------------------------------------------
01 // counters包提供告警计数器的功能
02 package counters
03
04 // alertCounter是一个未公开的类型
05 // 这个类型用于保存告警计数
06 type alertCounter int
listing64.go
-----------------------------------------------------------------------
01 // 这个示例程序展示无法从另一个包里
02 // 访问未公开的标识符
03 package main
04
05 import (
06 "fmt"
07
08 "github.com/goinaction/code/chapter5/listing64/counters"
09 )
10
11 // main是应用程序的入口
12 func main() {
13 // 创建一个未公开的类型的变量
14 // 并将其初始化为10
15 counter := counters.alertCounter(10)
16
17 // ./listing64.go:15: 不能引用未公开的名字
18 // counters.alertCounter
19 // ./listing64.go:15: 未定义:counters.alertCounter
20
21 fmt.Printf("Counter: %d\n", counter)
22 }
这个示例程序有两个代码文件。一个代码文件名字为counters.go,保存在 counters
包里;另一个代码文件名字为listing64.go,导入了 counters
包。让我们先从 counters
包里的代码开始,如代码清单5-65所示。
代码清单5-65 counters
/counters.go
01 // counters包提供告警计数器的功能
02 package counters
03
04 // alertCounter是一个未公开的类型
05 // 这个类型用于保存告警计数
06 type alertCounter int
代码清单5-65展示了只属于 counters
包的代码。你可能会首先注意到第02行。直到现在,之前所有的示例程序都使用了 package main
,而这里用到的是 package counters
。当要写的代码属于某个包时,好的实践是使用与代码所在文件夹一样的名字作为包名。所有的Go工具都会利用这个习惯,所以最好遵守这个好的实践。
在 counters
包里,我们在第06行声明了唯一一个名为 alertCounter
的标识符。这个标识符是一个使用 int
作为基础类型的类型。需要注意的是,这是一个未公开的标识符。
当一个标识符的名字以小写字母开头时,这个标识符就是未公开的,即包外的代码不可见。如果一个标识符以大写字母开头,这个标识符就是公开的,即被包外的代码可见。让我们看一下导入这个包的代码,如代码清单5-66所示。
代码清单5-66 listing64.go
01 // 这个示例程序展示无法从另一个包里
02 // 访问未公开的标识符
03 package main
04
05 import (
06 "fmt"
07
08 "github.com/goinaction/code/chapter5/listing64/counters"
09 )
10
11 // main是应用程序的入口
12 func main() {
13 // 创建一个未公开的类型的变量
14 // 并将其初始化为10
15 counter := counters.alertCounter(10)
16
17 // ./listing64.go:15: 不能引用未公开的名字
18 // counters.alertCounter
19 // ./listing64.go:15: 未定义:counters.alertCounter
20
21 fmt.Printf("Counter: %d\n", counter)
22 }
代码清单5-66中的listing64.go的代码在第03行声明了 main
包,之后在第08行导入了 counters
包。在这之后,我们跳到 main
函数里的第15行,如代码清单5-67所示。
代码清单5-67 listing64.go:第13到19行
13 // 创建一个未公开的类型的变量
14 // 并将其初始化为10
15 counter := counters.alertCounter(10)
16
17 // ./listing64.go:15: 不能引用未公开的名字
18 // counters.alertCounter
19 // ./listing64.go:15: 未定义:counters.alertCounter
在代码清单5-67的第15行,代码试图创建未公开的 alertCounter
类型的值。不过这段代码会造成第15行展示的编译错误,这个编译错误表明第15行的代码无法引用 counters.alertCounter
这个未公开的标识符。这个标识符是未定义的。
由于 counters
包里的 alertCounter
类型是使用小写字母声明的,所以这个标识符是未公开的,无法被listing64.go的代码访问。如果我们把这个类型改为用大写字母开头,那么就不会产生编译器错误。让我们看一下新的示例程序,如代码清单5-68所示,这个程序在 counters
包里实现了工厂函数。
代码清单5-68 listing68/
counters/counters.go
-----------------------------------------------------------------------
01 // counters包提供告警计数器的功能
02 package counters
03
04 // alertCounter是一个未公开的类型
05 // 这个类型用于保存告警计数
06 type alertCounter int
07
08 // New创建并返回一个未公开的
09 // alertCounter类型的值
10 func New(value int) alertCounter {
11 return alertCounter(value)
12 }
listing68.go
-----------------------------------------------------------------------
01 // 这个示例程序展示如何访问另一个包的未公开的
02 // 标识符的值
03 package main
04
05 import (
06 "fmt"
07
08 "github.com/goinaction/code/chapter5/listing68/counters"
09 )
10
11 // main是应用程序的入口
12 func main() {
13 // 使用counters包公开的New函数来创建
14 // 一个未公开的类型的变量
15 counter := counters.New(10)
16
17 fmt.Printf("Counter: %d\n", counter)
18 }
这个例子已经修改为使用工厂函数来创建一个未公开的 alertCounter
类型的值。让我们先看一下 counters
包的代码,如代码清单5-69所示。
代码清单5-69 counters
/counters.go
01 // counters包提供告警计数器的功能
02 package counters
03
04 // alertCounter是一个未公开的类型
05 // 这个类型用于保存告警计数
06 type alertCounter int
07
08 // New创建并返回一个未公开的
09 // alertCounter类型的值
10 func New(value int) alertCounter {
11 return alertCounter(value)
12 }
代码清单5-69展示了我们对 counters
包的改动。 alertCounter
类型依旧是未公开的,不过现在在第10行增加了一个名为 New
的新函数。将工厂函数命名为 New
是Go语言的一个习惯。这个 New
函数做了些有意思的事情:它创建了一个未公开的类型的值,并将这个值返回给调用者。让我们看一下listing68.go的 main
函数,如代码清单5-70所示。
代码清单5-70 listing68.go
11 // main是应用程序的入口
12 func main() {
13 // 使用counters包公开的New函数来创建
14 // 一个未公开的类型的变量
15 counter := counters.New(10)
16
17 fmt.Printf("Counter: %d\n", counter)
18 }
在代码清单5-70的第15行,可以看到对 counters
包里 New
函数的调用。这个 New
函数返回的值被赋给一个名为 counter
的变量。这个程序可以编译并且运行,但为什么呢? New
函数返回的是一个未公开的 alertCounter
类型的值,而 main
函数能够接受这个值并创建一个未公开的类型的变量。
要让这个行为可行,需要两个理由。第一,公开或者未公开的标识符,不是一个值。第二,短变量声明操作符,有能力捕获引用的类型,并创建一个未公开的类型的变量。永远不能显式创建一个未公开的类型的变量,不过短变量声明操作符可以这么做。
让我们看一个新例子,这个例子展示了这些可见的规则是如何影响到结构里的字段,如代码清单5-71所示。
代码清单5-71 listing71/
entities/entities.go
-----------------------------------------------------------------------
01 // entities包包含系统中
02 // 与人有关的类型
03 package entities
04
05 // User在程序里定义一个用户类型
06 type User struct {
07 Name string
08 email string
09 }
listing71.go
-----------------------------------------------------------------------
01 // 这个示例程序展示公开的结构类型中未公开的字段
02 // 无法直接访问
03 package main
04
05 import (
06 "fmt"
07
08 "github.com/goinaction/code/chapter5/listing71/entities"
09 )
10
11 // main是应用程序的入口
12 func main() {
13 // 创建entities包中的User类型的值
14 u := entities.User{
15 Name: "Bill",
16 email: "bill@email.com",
17 }
18
19 // ./example69.go:16: 结构字面量中结构entities.User
20 // 的字段’email’未知
21
22 fmt.Printf("User: %v\n", u)
23 }
代码清单5-71中的代码有一些微妙的变化。现在我们有一个名为 entities
的包,声明了名为 User
的结构类型,如代码清单5-72所示。
代码清单5-72 entities
/entities.go
01 // entities包包含系统中
02 // 与人有关的类型
03 package entities
04
05 // User在程序里定义一个用户类型
06 type User struct {
07 Name string
08 email string
09 }
代码清单5-72的第06行中的 User
类型被声明为公开的类型。 User
类型里声明了两个字段,一个名为 Name
的公开的字段,一个名为 email
的未公开的字段。让我们看一下listing71.go的代码,如代码清单5-73所示。
代码清单5-73 listing71.go
01 // 这个示例程序展示公开的结构类型中未公开的字段
02 // 无法直接访问
03 package main
04
05 import (
06 "fmt"
07
08 "github.com/goinaction/code/chapter5/listing71/entities"
09 )
10
11 // main是程序的入口
12 func main() {
13 // 创建entities包中的User类型的值
14 u := entities.User{
15 Name: "Bill",
16 email: "bill@email.com",
17 }
18
19 // ./example69.go:16: 结构字面量中结构entities.User
20 // 的字段'email'未知
21
22 fmt.Printf("User: %v\n", u)
23 }
代码清单5-73的第08行导入了 entities
包。在第14行声明了 entities
包中的公开的类型 User
的名为 u
的变量,并对该字段做了初始化。不过这里有一个问题。第16行的代码试图初始化未公开的字段 email
,所以编译器抱怨这是个未知的字段。因为 email
这个标识符未公开,所以它不能在 entities
包外被访问。
让我们看最后一个例子,这个例子展示了公开和未公开的内嵌类型是如何工作的,如代码清单5-74所示。
代码清单5-74 listing74/
entities/entities.go
-----------------------------------------------------------------------
01 // entities包包含系统中
02 // 与人有关的类型
03 package entities
04
05 // user在程序里定义一个用户类型
06 type user struct {
07 Name string
08 Email string
09 }
10
11 // Admin在程序里定义了管理员
12 type Admin struct {
13 user // 嵌入的类型是未公开的
14 Rights int
15 }
listing74.go
-----------------------------------------------------------------------
01 // 这个示例程序展示公开的结构类型中如何访问
02 // 未公开的内嵌类型的例子
03 package main
04
05 import (
06 "fmt"
07
08 "github.com/goinaction/code/chapter5/listing74/entities"
09 )
10
11 // main是应用程序的入口
12 func main() {
13 // 创建entities包中的Admin类型的值
14 a := entities.Admin{
15 Rights: 10,
16 }
17
18 // 设置未公开的内部类型的
19 // 公开字段的值
20 a.Name = "Bill"
21 a.Email = "bill@email.com"
22
23 fmt.Printf("User: %v\n", a)
24 }
现在,在代码清单5-74里, entities
包包含两个结构类型,如代码清单5-75所示。
代码清单5-75 entities
/entities.go
01 // entities包包含系统中
02 // 与人有关的类型
03 package entities
04
05 // user在程序里定义一个用户类型
06 type user struct {
07 Name string
08 Email string
09 }
10
11 // Admin在程序里定义了管理员
12 type Admin struct {
13 user // 嵌入的类型未公开
14 Rights int
15 }
在代码清单5-75的第06行,声明了一个未公开的结构类型 user
。这个类型包括两个公开的字段 Name
和 Email
。在第12行,声明了一个公开的结构类型 Admin
。 Admin
有一个名为 Rights
的公开的字段,而且嵌入一个未公开的 user
类型。让我们看一下listing74.go的 main
函数,如代码清单5-76所示。
代码清单5-76 listing74.go:第11到24行
11 // main是应用程序的入口
12 func main() {
13 // 创建entities包中的Admin类型的值
14 a := entities.Admin{
15 Rights: 10,
16 }
17
18 // 设置未公开的内部类型的
19 // 公开字段的值
20 a.Name = "Bill"
21 a.Email = "bill@email.com"
22
23 fmt.Printf("User: %v\n", a)
24 }
让我们从代码清单5-76的第14行的 main
函数开始。这个函数创建了 entities
包中的 Admin
类型的值。由于内部类型 user
是未公开的,这段代码无法直接通过结构字面量的方式初始化该内部类型。不过,即便内部类型是未公开的,内部类型里声明的字段依旧是公开的。既然内部类型的标识符提升到了外部类型,这些公开的字段也可以通过外部类型的字段的值来访问。
因此,在第20行和第21行,来自未公开的内部类型的字段 Name
和 Email
可以通过外部类型的变量 a
被访问并被初始化。因为 user
类型是未公开的,所以这里没有直接访问内部类型。